iT邦幫忙

0

Nextjs: Hydration failed because the initial UI does not match what was rendered on the server

  • 分享至 

  • xImage
  •  

這是Reactv18開始有的問題,官方描述:

While rendering your application, there was a difference between the React tree that was pre-rendered (SSR/SSG) and the React tree that rendered during the first render in the Browser. The first render is called Hydration which is a feature of React

我的理解是:

因為Nextjs啟動時會同時產生Server Side與Client Side (兩個都包含React tree),Server端React tree 是在 pre-rendered (SSR/SSG) 產生的,而Client端React Tree是first render in the Browser (又叫做Hydration),當兩者認知的React Tree不一樣時就會發生此錯誤!!!
(不一樣會導致 React Tree與 DOM 不同步,並導致出現意外的內容/屬性。)

這個問題對應的情境其實有好幾個,我先列我遇到的:

情境1: 有動態更新資料的Component

Whenever you are creating a component that has dynamic data. Try importing that component in below mentioned format. It disables SSR and lets you render new data on API call.
(意思是如果該Component需要動態撈資料,則透過dymanic關閉SSR渲染達到動態目的,避免Server端一開始先渲染導致與Client不一樣而衝突)

//===============React18版本會出錯==============//
import ProductCard from "./Product"; //這個Component會動態去載入圖片

//================可以運作的方法=================//
// Whenever you are creating a component that has dynamic data. Try importing that component in below mentioned format. 
// It disables SSR and lets you render new data on API call.
import dynamic from 'next/dynamic';
const ProductCard = dynamic(() => import("./Product"), {
    ssr: false,
});


const ProductHome = () => {
  const router = useRouter();
  const { id } = router.query;

  if (!id) return <></>; //防止因為第一次渲染沒拿到id而出問題

  const product = getProductById(id as string);

  return (
    <div>
        ...  
        <ProductCard product={product} all />
    </div>
  );
};

另外,我也想到另外可以使用Nextjs提供的SSG功能,讓需要動態撈取資料的部份先放到getStaticProps中,讓Server端在第一次渲染之前就先獲取資料,然後再建置出完整的html檔案。

import {GetStaticProps} from 'next';

interface Post {
  userId: number;
  id: number;
  title: string;
  body: string;
}

export const getStaticProps:GetStaticProps = async ()=>{
  const res = await fetch("https://jsonplaceholder.typicode.com/posts/1");
  const post: Post = await res.json();
  return {
    props: {
      post,
    },
  }
}

這個方法我沒有實際試過但我認為應該可行!

以下是官方列的問題情境:

情境2: 使用不正當的預先動作

下方是個簡單範例告訴你當Client端與Server端分開渲染會發生什麼事

function MyComponent() {
  // This condition depends on `window`. 
  // During the first render of the browser the `color` variable will be different
  const color = typeof window !== 'undefined' ? 'red' : 'blue'
  
  // As color is passed as a prop there is a mismatch 
  // between what was rendered server-side vs what was rendered in the first render
  return <h1 className={`title ${color}`}>Hello World!</h1>
}

上方color在server端會是”blue”,但在Client端 (Browser)會是 “red”!!!

解決方法是使用useState()與useEffect(),依據官方描述,因為useEffect()只會在Client端運作,所以可以確保先後順序是Client觸發了,Client與Server再一起改變!! (而不是兩者先各做各的)

// In order to prevent the first render from being different 
// you can use `**useEffect`** which is only executed in the browser 
// and is executed during hydration
import { useEffect, useState } from 'react'

function MyComponent() {
  // The default value is 'blue', 
  // it will be used during pre-rendering 
  // and the first render in the browser (hydration)
  const [color, setColor] = useState('blue')
  
  // During hydration `useEffect` is called. `window` is available in `useEffect`. 
  // In this case because we know we're in the browser checking for window is not needed.
  // If you need to read something from window that is fine.
  // By calling `setColor` in `useEffect` a render is triggered after hydrating, 
  // this causes the "browser specific" value to be available. In this case 'red'.
  useEffect(() => {
		setColor('red')
	}, [])
  
  // As color is a state passed as a prop there is no mismatch 
  // between what was rendered server-side vs what was rendered in the first render. 
  // After useEffect runs the color is set to 'red'
  return <h1 className={`title ${color}`}>Hello World!</h1>
}

上面範例的意思是,因為使用useState,所以Server與Client一開始都會看到color是”Blue”!!!

當前後端渲染完成後,由Client觸發useEffect()做componentDidMount()觸發setColor(),改變color的同時前後端也會同步,因此兩者對於color的認知都會變成color=”red”!!!

情境3: 使用不正當的HTML結構

Invalid HTML may cause hydration mismatch (such as div inside p)

這個意思是說,如果在HTML結構上使用法不正確,可能會導致Client與Server看到的結果不一樣。

我的認知是,因為Client是在Browser上運作,受限於Browser規範導致錯誤的HTML在Client會以不一樣的姿態渲染,導致兩者不同。

官方範例:

return (
    <p>
      <div>
        This is not correct and should never be done because the p tag has been
        abused
      </div>
      <Image src="/vercel.svg" alt="" width="30" height="30" />
    </p>
  )
}

修復方式就是用正確的邏輯撰寫:

export const CorrectComponent = () => {
  return (
    <div>
      <div>
        This is correct and should work because a div is really good for this
        task.
      </div>
      <Image src="/vercel.svg" alt="" width="30" height="30" />
    </div>
  )

官方列出可能發生在css-in-js類型的libraries:

  • When using Styled Components / Emotion
  • When using other css-in-js libraries

參考資料:
https://github.com/vercel/next.js/discussions/35773#discussioncomment-2840696
https://nextjs.org/docs/messages/react-hydration-error


圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

1 則留言

0
AndrewYEE
iT邦新手 3 級 ‧ 2023-02-13 16:31:25

第一次發文,還請多多指教

我要留言

立即登入留言